Skip to content

Expose BaseDirection through paragraph analysis - #708

Merged
waywardmonkeys merged 1 commit into
linebender:mainfrom
waywardmonkeys:feature/base-direction
Jul 24, 2026
Merged

Expose BaseDirection through paragraph analysis#708
waywardmonkeys merged 1 commit into
linebender:mainfrom
waywardmonkeys:feature/base-direction

Conversation

@waywardmonkeys

Copy link
Copy Markdown
Contributor

Parley previously always inferred the paragraph embedding level from the text, even when a caller knew the surrounding reading direction. This is especially wrong for numeric or neutral text and for mixed-direction text whose first strong character does not represent the paragraph direction.

Unicode UAX #9 rule HL1 explicitly permits a higher-level protocol to set the paragraph embedding level instead of applying the P2/P3 first-strong heuristic. This is also the model exposed by HTML dir="ltr", dir="rtl", and dir="auto", and by CSS direction. Supplying a base direction is not a bidi override: Latin and numeric runs remain LTR inside an RTL paragraph, while neutral resolution, visual ordering, alignment, and editing use the requested paragraph level.

Add BaseDirection to AnalysisOptions, default it to Auto, and use it in BidiResolver. Explicit RTL analysis now bypasses the all-LTR content fast path so neutral and numeric text receives complete bidi levels, and empty text retains its requested base direction. Re-export the shared parlance::BaseDirection, add Analysis::is_rtl, and expose set_base_direction on all three Parley builders. A private shared builder-options value keeps the new paragraph option and the existing build settings on one path.

This updates the unreleased low-level API: replace None, Some(0), and Some(1) passed to BidiResolver::resolve with BaseDirection::Auto, BaseDirection::Ltr, and BaseDirection::Rtl. Existing AnalysisOptions literals can add base_direction or use ..AnalysisOptions::default().

Paired Tango benchmarks against main found no consistent layout regression. The first 8-second full comparison put all 18 layout cases between -1.33% and +0.79%; a repeat ranged from -0.39% to +1.79% without a consistent pattern. A focused 20-second comparison measured the shortest default Latin case at +0.00% and the styled Latin case at -1.05%.

Spec references:

@waywardmonkeys

Copy link
Copy Markdown
Contributor Author

Assisted by GPT 5.6 Sol high. I reviewed it.

@waywardmonkeys

Copy link
Copy Markdown
Contributor Author

This came about during development of Overstory and is needed there.

@nicoburns nicoburns left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense to me. I believe I will eventually need a span-level override for web compat (at which point it probably makes sense to make it a style). But I imagine that's a lot more involved to implement.

@waywardmonkeys

Copy link
Copy Markdown
Contributor Author

There's plenty of future work to do on bidi for sure. But this is fixing something really basic / simple / trivial.

@tomcur tomcur left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, thank you!

Comment on lines +83 to +87
/// Whether the paragraph's resolved base direction is right-to-left.
#[inline(always)]
pub fn is_rtl(&self) -> bool {
!self.paragraph_level.is_multiple_of(2)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd normally suggest dropping this method, as it's derivable from Analysis::paragraph_level (and having both methods introduces some mental overhead of whether the two methods are the same).

But, users having to is_multiple_of(2) really isn't so nice, so I'll open a follow-up proposal instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's more issues there than that ... paragraph level has at least 2 names in the code. :)

Parley previously always inferred the paragraph embedding level from the text, even when a caller knew the surrounding reading direction. This is especially wrong for numeric or neutral text and for mixed-direction text whose first strong character does not represent the paragraph direction.

Unicode UAX linebender#9 rule HL1 explicitly permits a higher-level protocol to set the paragraph embedding level instead of applying the P2/P3 first-strong heuristic. This is also the model exposed by HTML `dir="ltr"`, `dir="rtl"`, and `dir="auto"`, and by CSS `direction`. Supplying a base direction is not a bidi override: Latin and numeric runs remain LTR inside an RTL paragraph, while neutral resolution, visual ordering, alignment, and editing use the requested paragraph level.

Add `BaseDirection` to `AnalysisOptions`, default it to `Auto`, and use it in `BidiResolver`. Explicit RTL analysis now bypasses the all-LTR content fast path so neutral and numeric text receives complete bidi levels, and empty text retains its requested base direction. Re-export the shared `parlance::BaseDirection`, add `Analysis::is_rtl`, and expose `set_base_direction` on all three Parley builders. A private shared builder-options value keeps the new paragraph option and the existing build settings on one path.

This updates the unreleased low-level API: replace `None`, `Some(0)`, and `Some(1)` passed to `BidiResolver::resolve` with `BaseDirection::Auto`, `BaseDirection::Ltr`, and `BaseDirection::Rtl`. Existing `AnalysisOptions` literals can add `base_direction` or use `..AnalysisOptions::default()`.

Paired Tango benchmarks against `main` found no consistent layout regression. The first 8-second full comparison put all 18 layout cases between -1.33% and +0.79%; a repeat ranged from -0.39% to +1.79% without a consistent pattern. A focused 20-second comparison measured the shortest default Latin case at +0.00% and the styled Latin case at -1.05%.

Spec references:

- https://www.unicode.org/reports/tr9/#HL1

- https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute

- https://www.w3.org/TR/css-writing-modes-3/#text-direction
@waywardmonkeys
waywardmonkeys force-pushed the feature/base-direction branch from d280506 to 2663edf Compare July 24, 2026 13:39
@waywardmonkeys
waywardmonkeys enabled auto-merge July 24, 2026 13:41
@waywardmonkeys
waywardmonkeys added this pull request to the merge queue Jul 24, 2026
Merged via the queue into linebender:main with commit 9c41a4d Jul 24, 2026
24 checks passed
@waywardmonkeys
waywardmonkeys deleted the feature/base-direction branch July 24, 2026 13:59
@tomcur tomcur mentioned this pull request Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants